<?php

/*
 * Copyright 2008 the original author or authors.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#namespace tehframework\annotations\builder;

#use \InvalidArgumentException;
#use \ReflectionClass;
#use tehframework\annotations\AnnotationDefinition;
#use tehframework\annotations\AnnotationBuilderAware;

class AnnotationBuilderImpl extends AnnotationBuilder
{
	/**
	 * @var bool
	 */
	private $autoload;
	
	/**
	 * @var array<string, string>
	 */
	private $annotationToClassMap = array();
	
	/**
	 * @var array<string>
	 */
	private $namespaces = array();
	
	/**
	 * @var bool
	 */
	private $buildNestedAnnotations;
	
	/**
	 * Constructs a new default annotation builder.
	 * 
	 * @param  bool $autoload
	 *         Whether to use autoload of annotation classes.
	 * @param  array<string>
	 *         An array of namespaces where annotation classes can be found.
	 * @param  array<string, string>? $annotationToClassMap
	 *         An array mapping annotation names to class names.
	 */
	public function __construct(
		$autoload = false,
		array $namespaces = array(),
		array $annotationToClassMap = array())
	{
		$this->autoload = (bool)$autoload;
		
		$this
			->addNamespaces($namespaces)
			->mapAnnotationsToClasses($annotationToClassMap)
			->disableNestedAnnotations();
	}
	
	/**
	 * Enables autoload of annotation classes.
	 * 
	 * @return AnnotationBuilderImpl
	 */
	public function enableAutoload()
	{
		$this->autoload = true;
		
		return $this;
	}
	
	/**
	 * Disables autoload of annotation classes.
	 * 
	 * @return AnnotationBuilderImpl
	 */
	public function disableAutoload()
	{
		$this->autoload = false;
		
		return $this;
	}
	
	/**
	 * Maps an annotation name to a class name.
	 * 
	 * @param  string $annotationName
	 *         A name of the annotation without 'Annotation' suffix.
	 * @param  string $className
	 *         A name of the existing and already loaded class.
	 * @return AnnotationBuilderImpl
	 * @throws InvalidArgumentException
	 *         If the specified annotation name is not a string or is empty.
	 * @throws InvalidArgumentException
	 *         If the specified class could not be found without autoloading.
	 */
	public function mapAnnotationToClass($annotationName, $className)
	{
		if (empty($annotationName) || !is_string($annotationName))
		{
			throw new InvalidArgumentException(
				'$annotationName must be a non-empty string.'
			);
		}
		
		if (!class_exists($className, false))
		{
			throw new InvalidArgumentException(sprintf(
				'Can not map [%s] annotation to non-existing class: [%s]. ' .
				'Forgot to load it?', $annotationName, $className
			));
		}
		
		if ((strlen($annotationName) !== 10) &&
			(substr($annotationName, -10) === 'Annotation'))
		{
			$annotationName = substr($annotationName, 0, -10);
		}
		
		$this->annotationToClassMap[$annotationName] = $className;
		
		return $this;
	}
	
	/**
	 * Maps the specified tag names to class names.
	 * 
	 * @param  array<string, string> $tagToClassMap
	 * @return AnnotationBuilderImpl
	 * @throws InvalidArgumentException
	 *         If any tag name in the specified array is not a string
	 *         or is empty.
	 * @throws InvalidArgumentException
	 *         If any class in the specified array could not be found.
	 */
	public function mapAnnotationsToClasses(array $annotationToClassMap)
	{
		foreach ($annotationToClassMap as $annotationName => $className)
		{
			$this->mapAnnotationToClass($annotationName, $className);
		}
		
		return $this;
	}
	
	/**
	 * Adds a new namespace where annotation classes can be found.
	 * 
	 * @param  string $namespaceName
	 * @return AnnotationBuilderImpl
	 */
	public function addNamespace($namespaceName)
	{
		$this->namespaces[] = (string)$namespaceName;
		
		return $this;
	}
	
	/**
	 * Adds namespaces where annotation classes can be found from
	 * the specified array.
	 * 
	 * @param  array<string> $namespaceNames
	 * @return AnnotationBuilderImpl
	 */
	public function addNamespaces(array $namespaceNames)
	{
		foreach ($namespaceNames as $namespaceName)
		{
			$this->addNamespace($namespaceName);
		}
		
		return $this;
	}
	
	/**
	 * Enables building of nested annotations.
	 * 
	 * Prior to building the actual annotation from the definition specified
	 * to the buildAnnotation() method, all definitions contained in argument
	 * and property arrays will be built.
	 * 
	 * @return AnnotationBuilderImpl
	 */
	public function enableNestedAnnotations()
	{
		$this->buildNestedAnnotations = true;
		
		return $this;
	}
	
	/**
	 * Disables building of nested annotations.
	 *
	 * Annotation definitions contained in argument and property arrays of the
	 * definition provided to the buildAnnotation() method will not be built.
	 * 
	 * @return AnnotationBuilderImpl
	 */
	public function disableNestedAnnotations()
	{
		$this->buildNestedAnnotations = false;
		
		return $this;
	}
	
	/**
	 * Returns an instance of Annotation built from the specified definition.
	 *
	 * @param  AnnotationDefinition $annotationDefinition
	 *         A definition of annotation to build.
	 * @return Annotation
	 *         An instance of Annotation from the specified definition.
	 * @throws BuilderException
	 */
	public function buildAnnotation(AnnotationDefinition $annotationDefinition)
	{
		$arguments  = $annotationDefinition->arguments;
		$properties = $annotationDefinition->properties;
		
		if ($this->buildNestedAnnotations)
		{
			if (!empty($arguments))
			{
				$this->buildNestedAnnotations($arguments);
			}
			
			if (!empty($properties))
			{
				$this->buildNestedAnnotations($properties);
			}
		}
		
		$nameClass = $this->resolveClassName(
			$annotationDefinition->name, !empty($annotationDefinition->type));
		$typeClass = empty($annotationDefinition->type)
			? $nameClass
			: $this->resolveClassName($annotationDefinition->type);
		
		$class = new ReflectionClass($typeClass);
		
		if ($class->implementsInterface('Annotation') === false)
		{
			throw new BuilderException(sprintf(
				'[%s] can not be used for [%s] annotation, ' . 
				'because it does not implement Annotation interface.',
				$class->getName(), $annotationDefinition->name
			));
		}
		
		if (($typeClass !== $nameClass) &&
			($class->isSubclassOf($nameClass) === false))
		{
			throw new BuilderException(sprintf(
				'[%s] annotation must be of type [%s].', $typeClass, $nameClass
			));
		}
		
		$annotation = empty($arguments)
			? $class->newInstance() : $class->newInstanceArgs($arguments);
		
		if (!empty($properties))
		{
			$this->injectProperties($properties, $annotation, $class);
		}
		
		if ($annotation instanceof AnnotationBuilderAware)
		{
			$annotation->setAnnotationBuilder($this);
		}
		
		return $annotation;
	}
	
	/**
	 * @param array $vars
	 */
	private function buildNestedAnnotations(&$vars)
	{
		foreach ($vars as &$var)
		{
			if ($var instanceof AnnotationDefinition)
			{
				$var = $this->buildAnnotation($var);
			}
			elseif (is_array($var))
			{
				$this->buildNestedAnnotations($var);
			}
		}
	}
	
	/**
	 * @param  string $class
	 * @param  bool $allowInterfaces
	 * @return string
	 * @throws BuilderException
	 */
	private function resolveClassName($class, $allowInterfaces = false)
	{
		if (substr($class, -10) === 'Annotation')
		{
			$nameWithSuffix    = $class;
			$nameWithoutSuffix = strlen($class) === 10
				? $class : substr($class, 0, -10);  
		}
		else
		{
			$nameWithSuffix    = $class . 'Annotation';
			$nameWithoutSuffix = $class;
		}
		
		if (isset($this->annotationToClassMap[$nameWithoutSuffix]))
		{
			return $this->annotationToClassMap[$nameWithoutSuffix];
		}
		
		if (class_exists($nameWithSuffix, $this->autoload) ||
			($allowInterfaces && interface_exists($nameWithSuffix)))
		{
			return $nameWithSuffix;
		}
		
		foreach ($this->namespaces as $ns)
		{
			$type = $ns . T_NS_SEPARATOR . $nameWithSuffix;
			
			if (class_exists($type, $this->autoload) ||
				($allowInterfaces && interface_exists($type)))
			{
				return $type;
			}
		}
		
		throw new BuilderException(sprintf(
			'Can not find a class for [%s] annotation.', $nameWithoutSuffix
		));
	}
	
	/**
	 * @param  array $properties
	 * @param  Annotation $annotation
	 * @param  ReflectionClass $class
	 * @throws BuilderException
	 */
	private function injectProperties(
		array $properties, Annotation $annotation, ReflectionClass $class)
	{
		foreach ($properties as $propertyName => $value)
		{
			/* @var $method ReflectionMethod */
			$method = $class->hasMethod('set' . $propertyName)
				? $class->getMethod('set' . $propertyName) : null;
			
			if ($method && $method->isPublic() &&
				($method->getNumberOfRequiredParameters() === 1))
			{
				$method->invoke($annotation, $value);
				
				continue;
			}
			
			if (isset($annotation->{$propertyName}))
			{
				$annotation->{$propertyName} = $value;
				
				continue;
			}
			
			/* @var $property ReflectionProperty */
			$property = $class->hasProperty($propertyName)
				? $class->getProperty($propertyName) : null;
			
			if ($property && $property->isPublic())
			{
				$property->setValue($annotation, $value);
				
				continue;
			}
			
			throw new BuilderException(sprintf(
				'Can not set a value for [%s] property: [%s]. ' .
				'No setter method or public property defined.',
				$propertyName, $value
			));
		}
	}
}